package vq.codec.managers;

import java.util.ArrayList;
import java.util.List;

import vq.codec.entities.Dat;
import vq.codec.entities.Dictionary;
import vq.codec.entities.EncodeMethod;
import vq.codec.exception.DecodingException;
import vq.codec.exception.EncodingException;
import vq.codec.profiling.Profiler;
import vq.codec.util.Util;

public class VQCodecManagerImpl extends VQCodecManager {

	private static VQCodecManagerImpl instance = new VQCodecManagerImpl();
	
	public static VQCodecManagerImpl getInstance() {
		return instance;
	}

	public List<Integer> encode(Dat source, Dictionary dictionary, 
			EncodeMethod encodeMethod) throws EncodingException {
		
		return encode(source, dictionary, encodeMethod, null);
	}

	public List<Integer> encode(Dat source, Dictionary dictionary, 
			EncodeMethod encodeMethod, Profiler profiler) throws EncodingException {
		
		List<Integer> toReturn = null;
		
		if (Util.isNull(source)) {
			throw new EncodingException("Erro ao tentar codificar. Dat inválido.");
		}
		
		if (Util.isNull(dictionary)) {
			throw new EncodingException("Erro ao tentar codificar. Dicionário inválido.");
		}
		
		switch (encodeMethod) {
		case FULL_SCAN:
			if (profiler != null) {
				profiler.startProfiling();
			}			
			
			toReturn = encodeFS(source, dictionary, profiler);
			
			if (profiler != null) {
				profiler.stopProfiling();
			}
			break;
		case PARTIAL_DISTANCE_SEARCH:
			if (profiler != null) {
				profiler.startProfiling();
			}
			
			toReturn = encodePDS(source, dictionary, profiler);
			
			if (profiler != null) {
				profiler.stopProfiling();
			}
			break;
		default:
			throw new EncodingException("Método para codificação inválido.");
		}
		
		return toReturn;
	}
	
	public Dat decode(List<Integer> encoded, Dictionary dictionary) 
		throws DecodingException{
		
		Dat dat = new Dat();
		
		dat.setGrayScale(255);
		dat.setHeight(encoded.size()/dictionary.getQuantizatorDimension());
		dat.setWidth(encoded.size()/dictionary.getQuantizatorDimension());
		
		for (Integer code : encoded) {
			List<Double> vector = dictionary.getDictionary().get(code);
			dat.getVectors().add(Util.doubleToIntegerList(vector));
		}
		
		return dat;
	}
	
	public double calculateMSE(Dat source, Dat encoded) {
		double mse = 0;
		for (int i = 0; i < source.getHeight()*source.getQuantizatorDimension(); i++) {
			for (int j = 0; j < source.getQuantizatorDimension(); j++) {
				mse += Math.pow((source.getVectors().get(i).get(j) - encoded.getVectors().get(i).get(j)), 2);
			}
		}
		mse = mse/((source.getHeight()*source.getQuantizatorDimension()) * source.getQuantizatorDimension());
		return mse;
	}

	public double calculatePSNR(Dat source, Dat encoded) {
		return 10 * Math.log10(Math.pow(source.getGrayScale(), 2)/calculateMSE(source, encoded));
	}
	
	private List<Integer> encodeFS(Dat source, Dictionary dictionary, Profiler profiler) {
		List<Integer> encoded = new ArrayList<Integer>();
		
		for (int i = 0; i < source.getVectors().size(); i++) {
			List<Integer> vector = source.getVectors().get(i);
			
			int closerNeighborCode = 0;
			List<Double> closerNeighbor = dictionary.getDictionary().get(0);
			double smallerEuclidQuadraticDistance = calculateEuclidQuadraticDistance(vector, closerNeighbor, profiler);
			for (int j = 1; j < dictionary.getDictionary().size(); j++) {
				List<Double> dictionaryVector = dictionary.getDictionary().get(j);
				
				double euclidQuadraticDistance = calculateEuclidQuadraticDistance(vector, dictionaryVector, profiler);
				
				if (euclidQuadraticDistance < smallerEuclidQuadraticDistance) {
					closerNeighborCode = j;
					smallerEuclidQuadraticDistance = euclidQuadraticDistance;
					closerNeighbor = dictionaryVector;
				}
				
				if (profiler != null) {
					profiler.incrementComparisonOperations();
				}
			}
			
			encoded.add(closerNeighborCode);
		}
		
		return encoded;
	}
	
	private List<Integer> encodePDS(Dat source, Dictionary dictionary, Profiler profiler) {

		List<Integer> encoded = new ArrayList<Integer>();
		
		for(int i = 0; i < source.getVectors().size(); i++){
			List<Integer> vector = source.getVectors().get(i);
			
			int closerNighboorCode = 0;
			List<Double> dictionaryVector = dictionary.getDictionary().get(0);
			double smallerEuclidQuadraticDistance = calculateEuclidQuadraticDistance(vector,dictionaryVector, profiler);
			
			for(int j = 1; j < dictionary.getDictionary().size(); j++){
				dictionaryVector = dictionary.getDictionary().get(j);
				
				double euclidQuadraticDistance = calculateEuclidQuadraticDistance(vector, dictionaryVector, profiler, smallerEuclidQuadraticDistance);
				
				if(euclidQuadraticDistance < smallerEuclidQuadraticDistance){
					closerNighboorCode = j;
					smallerEuclidQuadraticDistance = euclidQuadraticDistance;
				}
				
				if (profiler != null) {
					profiler.incrementComparisonOperations();
				}
			}
			
			encoded.add(closerNighboorCode);
			
		}
		
		return encoded;
	}
	
private double calculateEuclidQuadraticDistance(List<Integer> vector, 
			List<Double> dictionaryVector, Profiler profiler) {
		
		double euclidQuadraticDistance = 0;
		
		for (int j = 0; j < dictionaryVector.size(); j++) {
			euclidQuadraticDistance += Math.pow(vector.get(j) - dictionaryVector.get(j), 2);
			
			if (profiler != null) {
				profiler.incrementSumOperations();
				profiler.incrementSubtractOperations();
				profiler.incrementPotentiationOperations();
			}
		}
		
		return euclidQuadraticDistance;
	}
	
	private double calculateEuclidQuadraticDistance(List<Integer> vector, List<Double> dictionaryVector, 
													Profiler profiler,double smallerEuclidQuadraticDistance){
	
		double euclidQuadraticDistance = 0;
		
		for(int j = 0; j < dictionaryVector.size(); j++){
			euclidQuadraticDistance += Math.pow(vector.get(j)- dictionaryVector.get(j), 2);
			
			if (profiler != null) {
				profiler.incrementSumOperations();
				profiler.incrementSubtractOperations();
				profiler.incrementPotentiationOperations();
				profiler.incrementComparisonOperations();
			}
			if(euclidQuadraticDistance > smallerEuclidQuadraticDistance){
				return smallerEuclidQuadraticDistance;
			}
		}
		
		return euclidQuadraticDistance;
	}
	
}
